﻿using Manox.Libs.Networking;
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Manox.Libs.ODB
{
    /// <summary>
    /// Object database manager.
    /// </summary>
    public class ObjectDatabaseManager : MonoBehaviour
    {
        /// <summary>Items.</summary>
        private SortedList<Guid, ObjectDatabaseItem> Items;
        /// <summary>Objects count.</summary>
        public int ObjectsCount { get { lock (this.Items) return this.Items.Count; } }
                
        /// <summary>Item added.</summary>
        public event OnItemAddedCallback ItemAdded;
        /// <summary>Item added.</summary>
        public event OnItemRemovedCallback ItemRemoved;
        /// <summary>Item changed.</summary>
        public event OnItemDataChangedCallback ItemChanged;

        /// <summary>
        /// Indexer.
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public ObjectDatabaseItem this[Guid id]
        {
            get
            {
                lock (this.Items)
                    if (this.Items.ContainsKey(id))
                        return this.Items[id];

                return null;
            }
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        public ObjectDatabaseManager()
        {
            this.Items = new SortedList<Guid, ObjectDatabaseItem>();            
        }

        /// <summary>
        /// Add object.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="item"></param>
        public void AddObject(object sender, ObjectDatabaseItem item)
        {
            /// New item, not in ODB.
            if (item.Id == Guid.Empty)
                item.Id = Guid.NewGuid();

            Debug.Log("ODB object added : " + item + "  Guid : " + item.Id);

            lock (this.Items)
                this.Items.Add(item.Id, item);            

            if (this.ItemAdded != null)
            {
                ItemAddedEventArgs eventArgs = new ItemAddedEventArgs();
                eventArgs.NewItem = item;

                this.ItemAdded(sender, eventArgs);
            }

            item.InitializeInODB(sender, this);

            item.DataChanged += this.ObjectDatabaseItemDataChanged;
        }

        ///// <summary>
        ///// Add object.
        ///// </summary>
        ///// <param name="sender"></param>
        ///// <param name="item"></param>
        //internal void AddObjectFromNetwork(object sender, ObjectDatabaseItem item)
        //{
        //    /// New item, not in ODB.
        //    if (item.Id == Guid.Empty)
        //        item.Id = Guid.NewGuid();

        //    lock (this.Items)
        //        this.Items.Add(item.Id, item);

        //    item.DataChanged += this.ObjectDatabaseItemDataChanged;

        //    if (this.ItemAdded != null)
        //    {
        //        ItemAddedEventArgs eventArgs = new ItemAddedEventArgs();
        //        eventArgs.NewItem = item;

        //        this.ItemAdded(sender, eventArgs);
        //    }
        //}

        /// <summary>
        /// Remove object.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="item"></param>
        public void RemoveObject(object sender, ObjectDatabaseItem item)
        {
            lock (this.Items)
            {
                if (this.Items.ContainsKey(item.Id))
                {
                    ObjectDatabaseItem tempItem = this.Items[item.Id];

                    item.DataChanged -= this.ObjectDatabaseItemDataChanged;

                    this.Items.Remove(item.Id);

                    if (this.ItemRemoved != null)
                    {
                        ItemAddedEventArgs eventArgs = new ItemAddedEventArgs();
                        eventArgs.NewItem = item;

                        this.ItemAdded(sender, eventArgs);
                    }
                }
            }
        }

        /// <summary>
        /// Object database item data changed.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="eventArgs"></param>
        private void ObjectDatabaseItemDataChanged(object sender, ObjectDatabaseEventArgs eventArgs)
        {
            if (this.ItemChanged != null)
                this.ItemChanged(sender, eventArgs);
        }

        /// <summary>
        /// Get objects of type.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public List<T> GetObjectsOfType<T>() where T : ObjectDatabaseItem
        {
            List<T> output = new List<T>();

            lock (this.Items)
            {
                foreach (KeyValuePair<Guid, ObjectDatabaseItem> item in this.Items)
#if !UNITY_EDITOR && UNITY_WP8_1
                if (IntrospectionExtensions.GetTypeInfo(item.Value.GetType()).IsAssignableFrom(IntrospectionExtensions.GetTypeInfo(typeof(T))))
#else
                    if (item.Value.GetType().IsAssignableFrom(typeof(T)))
#endif
                        output.Add((T)item.Value);
            }

            return output;
        }     
        
        /// <summary>
        /// Foreach.
        /// </summary>
        /// <param name="action"></param>
        public void Foreach(Action<ObjectDatabaseItem> action)
        {
            this.ForeachAndRegister(action, null);
        }

        /// <summary>
        /// Foreach.
        /// </summary>
        /// <param name="action"></param>
        /// <param name="addedEventHandler"></param>
        public void ForeachAndRegister(Action<ObjectDatabaseItem> action, OnItemAddedCallback addedEventCallback)
        {
            lock (this.Items)
            {
                foreach (KeyValuePair<Guid, ObjectDatabaseItem> item in this.Items)
                    action(item.Value);

                if (addedEventCallback != null)
                    this.ItemAdded += addedEventCallback;
            }
        }

        /// <summary>
        /// Foreach.
        /// </summary>
        /// <param name="action"></param>
        public IEnumerable<T> Foreach<T>(Func<ObjectDatabaseItem, T> action)
        {
            lock (this.Items)
                foreach (KeyValuePair<Guid, ObjectDatabaseItem> item in this.Items)
                    yield return action(item.Value);
        }

        /// <summary>
        /// Get all synchronize events.
        /// </summary>
        /// <returns></returns>
        public Queue<ODBSynchronizationMessage> GetAllSynchronizeEvents()
        {
            Queue<ODBSynchronizationMessage> output = new Queue<ODBSynchronizationMessage>();

            lock (this.Items)
            {
                foreach (KeyValuePair<Guid, ObjectDatabaseItem> item in this.Items)
                {
                    ItemWithPropertiesAddedEventArgs eventArgs = new ItemWithPropertiesAddedEventArgs();
                    eventArgs.ItemAddedEventArgs = new ItemAddedEventArgs();
                    eventArgs.ItemAddedEventArgs.NewItem = item.Value;
                    eventArgs.Properties = item.Value.InitializingEventArgs();
                    
                    ODBSynchronizationMessage newMessage = new ODBSynchronizationMessage();
                    newMessage.EventArgs = eventArgs;

                    output.Enqueue(newMessage);
                }
            }

            return output;
        }
    }

    /// <summary>
    /// Item added event args.
    /// </summary>
    public class ItemAddedEventArgs : ObjectDatabaseEventArgs
    {
        /// <summary>New item.</summary>
        public ObjectDatabaseItem NewItem;


#if UNITY_NETWORK || STEAM_NETWORK
        /// <summary>
        /// Serialize.
        /// </summary>
        /// <param name="writer"></param>
        public override void Serialize(SerializeWriter writer)
        {
            base.Serialize(writer);

            writer.Write(this.NewItem.GetType().ToString());
            writer.Write(this.NewItem.Id);
        }

        /// <summary>
        /// Deserialize.
        /// </summary>
        /// <param name="reader"></param>
        public override void Deserialize(SerializeReader reader)
        {
            base.Deserialize(reader);

            string type = reader.ReadString();
            Guid id = reader.ReadGuid();

            this.NewItem = Activator.CreateInstance(Type.GetType(type)) as ObjectDatabaseItem;
            this.NewItem.Id = id;
        }
#endif          
    }

    /// <summary>
    /// On item added callback.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="eventArgs"></param>
    public delegate void OnItemAddedCallback(object sender, ItemAddedEventArgs eventArgs);

    /// <summary>
    /// On item removed callback.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="eventArgs"></param>
    public delegate void OnItemRemovedCallback(object sender, ItemAddedEventArgs eventArgs);

    /// <summary>
    /// On item added callback.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="eventArgs"></param>
    public delegate void OnItemDataChangedCallback(object sender, ObjectDatabaseEventArgs eventArgs);
}
